import random
import traceback
from datetime import datetime
from typing import Any

from flask import Flask

from task_service import AbsTaskService
from task_service.task_context import XpmContext
from x_pm import testend0820
from x_pm.db.in_service import LocTemplateService, LocTemplate, LocService, Loc, LocAllocatedService, \
    LocTemplateRatioService, LocTemplateRatio
from x_pm.db.out_service import FlexChangePlan, FlexChangePlanDtl, FlexLocNumChangeRecord, \
    LocTemplateRelation, PmOutService


def get_percent_value(value_list, precision=0):
    """
    计算数值百分比
    :param value_list:
    :param precision:
    :return:
    """
    total_sum = sum(val if val else 0 for val in value_list)
    if total_sum == 0:
        return [0 for _ in range(len(value_list))]

    digits = 10 ** precision
    votes_per_quota = [(val if val else 0) / total_sum * digits * 100 for val in value_list]

    target_seats = digits * 100

    seats = [int(votes) for votes in votes_per_quota]

    current_sum = sum(seats)

    remainder = [votes - seat for votes, seat in zip(votes_per_quota, seats)]

    while current_sum < target_seats:
        max_remainder = max(remainder)
        max_id = remainder.index(max_remainder)
        seats[max_id] += 1
        remainder[max_id] = 0
        current_sum += 1

    return [_ / 100 for _ in seats]


def parse_percent(item_list) -> list:
    item_dict = {}
    for item in item_list:
        length, width, height, heat_code, nums = item.split('_')
        if heat_code not in item_dict:
            item_dict[heat_code] = {}
        if height not in item_dict[heat_code]:
            item_dict[heat_code][height] = {}
        key = f'{length}_{width}'
        if key not in item_dict[heat_code][height]:
            item_dict[heat_code][height][key] = int(nums) if nums else 0
    # 按 热度、高度
    percent_dict = {}
    for heat_code in item_dict:
        for height in item_dict[heat_code]:
            key_list = []
            for k, v in item_dict[heat_code][height].items():
                length, width = k.split('_')
                key_list.append(((length, width), v))
            percent_list = get_percent_value([_[1] for _ in key_list])
            for idx, kv in enumerate(key_list):
                k, v = kv
                val = percent_list[idx]
                percent_dict[f'{k[0]}_{k[1]}_{height}_{heat_code}'] = val
    return [percent_dict['_'.join(_.split('_')[:-1])] for _ in item_list]


class LocExt(object):
    loc: Loc = None
    # 原始货位数量
    raw_spec_nums: int = None
    # 修改后货位数量
    result_spec_nums: int = None
    # 修改后货位模版
    result_supmode: str = None

    def __init__(self, loc: Loc):
        self.loc = loc

    def supmode_change(self):
        return self.loc.SUPMODE != self.result_supmode


class XPMTaskService(AbsTaskService):
    """
    排摸
    """
    name: str = 'PM'

    def __init__(self):
        # Loc模板信息(可用)
        self.loc_template: list[LocTemplate] = []
        self.loc_template_all: list[LocTemplate] = []
        # 反向查询字典  根据货格 -> 模板(高长宽) -> 查询模板ID
        self.loc_template_dict: dict[str, dict[str, str]] = {}
        self.loc_template_id_dict: dict[str, LocTemplate] = {}
        self.loc_template_all_id_dict: dict[str, LocTemplate] = {}

        # 所有模板类型
        self.available_goods_out: list[str] = []
        # 不同模板号的可放置的货物类型，以及每种货物类型最多放多少货物
        self.template_mapping: dict[str, dict[str, dict[str, int]]] = {}
        # 高、中、低热度 模板比例占用情况
        self.ahj, self.amj, self.alj = list[float](), list[float](), list[float]()

        # 模板比例
        self.tpl_ratio_dict = {}

        # 货格 货位占用情况
        self.las_dict: dict[str, int] = {}

        # 算法 总入参
        self.params_list: list = []
        self.params_item = []

        # 处理结果
        self.result: dict[str, dict[str, Any]] = {}

    def _pre_process(self):
        """
        预先获取 模板数据
        :return:
        """
        msg = f'[PM] [{self.context.task_id}] 预处理 开始, zone_list原始参数: {self.context.zone_list}'
        self.context.app.logger.info(msg)
        lts = LocTemplateService()
        self.loc_template_all = lts.get_template_data()
        self.loc_template = [_ for _ in self.loc_template_all if _.is_valid()]
        # 货架所有模板类型 (nums为0的 表示没有在使用)
        # 模板比例
        ltrs = LocTemplateRatioService()
        tpl_ratio_list: list[LocTemplateRatio] = ltrs.get_data()
        tpl_ratio_dict = {}
        for ratio in tpl_ratio_list:
            if ratio.spec not in tpl_ratio_dict:
                tpl_ratio_dict[ratio.spec] = {}
            # tpl_ratio_dict[ratio.spec][ratio.heat_code] = float(ratio.heat_ratio)
            tpl_ratio_dict[ratio.spec][ratio.heat_code] = float(ratio.ratio)
        self.tpl_ratio_dict = tpl_ratio_dict
        # 可用模板 及比例
        available_goods_out: list[str] = []
        ahj, amj, alj = [0], [0], [0]

        for k, v in tpl_ratio_dict.items():
            available_goods_out.append(k)
            ahj.append(v['A'])
            amj.append(v['B'])
            alj.append(v['C'])

        self.available_goods_out = available_goods_out
        self.ahj = ahj
        self.amj = amj
        self.alj = alj
        # 整理模版类型
        # default_dict = self.template_mapping[str(Config.DEFAULT_HEIGHT)]
        for lt in self.loc_template:
            # # 生成反向查询字典
            if lt.spec_no not in self.loc_template_dict:
                self.loc_template_dict[lt.spec_no] = {}
            self.loc_template_dict[lt.spec_no][lt.inner_spec_short()] = str(lt.id)
            # 生成Id字典
            self.loc_template_id_dict[str(lt.id)] = lt
        for lt in self.loc_template_all:
            self.loc_template_all_id_dict[str(lt.id)] = lt
        # 货格 货位占用情况
        las = LocAllocatedService()
        las_list = las.get_data()
        for las in las_list:
            self.las_dict[las.LOC] = las.loc_allocated
        # 处理 总输入参数
        # 依次处理每个巷道
        ls = LocService()
        zone_list = ls.get_putawayzone()
        params: dict = {}
        # 筛选后的zone_list
        parsed_zone_list = self.parse_zone_list(zone_list, self.context.zone_list)
        msg = f'[PM] [{self.context.task_id}] 预处理 zone_list匹配参数: {parsed_zone_list}'
        self.context.app.logger.info(msg)
        for zone in zone_list:
            if self.context.zone_list and zone not in parsed_zone_list:
                msg = f'[PM] [{self.context.task_id}] 预处理 巷道: {zone}, 跳过'
                self.context.app.logger.info(msg)
                continue
            zone_data = ls.get_zone_data(zone)
            msg = f'[PM] [{self.context.task_id}] 预处理 巷道: {zone}, 货格数量: {len(zone_data)}'
            self.context.app.logger.info(msg)
            template_mapping = {}
            for d in zone_data:
                height = str(d.HEIGHT)
                if height not in template_mapping:
                    template_mapping[height] = {}
                if d.spec_no not in template_mapping[height]:
                    template_mapping[height][d.spec_no] = {}
                    for lt in self.loc_template:
                        if lt.spec_no == d.spec_no:
                            template_mapping[height][d.spec_no][lt.inner_spec(d.HEIGHT)] = lt.nums
            row_list, sorted_zone_list = self.parse_row_list(zone_data)
            param = (
                row_list, len(row_list), self.available_goods_out, template_mapping, 100, self.ahj, self.amj, self.alj,
                zone)
            params[zone] = {'raw': sorted_zone_list, 'param': param, 'result': None}
        # 调用函数执行
        self.params_item = params.items()
        self.params_list = [_[1]['param'] for _ in self.params_item]
        msg = f'[PM] [{self.context.task_id}] 预处理 结束'
        self.context.app.logger.info(msg)

    def _process(self):
        msg = f'[PM] [{self.context.task_id}] 处理 开始'
        self.context.app.logger.info(msg)
        try:
            msg = f'[PM] [{self.context.task_id}] 算法调用 开始'
            self.context.app.logger.info(msg)
            selected_templates_list, ObjectiveValue_list = testend0820.solve(self.params_list, self.context.app.logger)
            # mock solve
            # selected_templates_list, ObjectiveValue_list = self.mock_solve(self.params_list)
            # 结果校验
            for selected_template in selected_templates_list:
                if isinstance(selected_template, int):
                    raise Exception(f'[PM] [{self.context.task_id}] 算法调用 无解')
            #
            raw_len = len(self.params_list)
            result_len = len([_ for _ in selected_templates_list if _])
            if len(self.params_item) != result_len or raw_len != result_len:
                raise Exception(
                    f'[PM] [{self.context.task_id}] 算法调用 输出异常，数据不完整. 预期巷道数: {raw_len}， 实际巷道数: {result_len}')
            for idx, p in enumerate(self.params_item):
                self.result[p[0]] = {'raw': p[1]['raw'], 'result': selected_templates_list[idx]}
        except BaseException as ex:
            msg = f'[PM] [{self.context.task_id}] 算法调用 排模算法执行异常, ex: {ex}'
            self.context.app.logger.error(msg)
            traceback.print_exception(ex)
        finally:
            msg = f'[PM] [{self.context.task_id}] 算法调用 结束'
            self.context.app.logger.info(msg)

    def mock_solve(self, params_list):
        """
        算法执行太慢，开发、调试时使用
        :param params_list:
        :return:
        """
        template_list = []
        value_list = []
        for param in params_list:
            x = [0] + [__[-1] + '*' + random.choice(list(self.loc_template_dict[__[2]].keys())) for __ in param[0]]
            template_list.append(x)
        return template_list, value_list

    def _post_process(self):
        """
        将最终结果写入数据库
        :return:
        """
        msg = f'[PM] [{self.context.task_id}] 后处理 开始'
        self.context.app.logger.info(msg)
        date_now = datetime.now()
        # 生成调整计划
        fcp = FlexChangePlan(self.context.fixed_raw_zone_list, 0, 0, 0, date_now)
        plan_id = fcp.id
        # 记录 要存储的数据 (按巷道划分)
        all_plan_dtl_list = []  # 总货位调整计划
        all_relation_list = []  # 总关系明细
        # 写入数据
        loc_ext_list: list[LocExt] = []
        for k, v in self.result.items():
            raw: list[Loc] = v['raw']
            result: dict[int, str] = v['result']
            #
            msg = f'[PM] [{self.context.task_id}] 后处理 巷道处理 {k}'
            self.context.app.logger.info(msg)
            # 调整计划明细
            plan_dtl_list: list[FlexChangePlanDtl] = []
            # 关系明细
            relation_list: list[LocTemplateRelation] = []
            for idx, loc in enumerate(raw):
                loc_ext = LocExt(loc)
                key1 = idx + 1
                inner_spec = result[key1]
                fixed_inner_spec = '*'.join(inner_spec.split('*')[1:])
                tpl_id = self.loc_template_dict[loc.spec_no][fixed_inner_spec]
                loc_ext.raw_spec_nums = self.loc_template_all_id_dict[
                    loc_ext.loc.SUPMODE].nums if loc_ext.loc.SUPMODE in self.loc_template_id_dict else 0
                loc_ext.result_spec_nums = self.loc_template_id_dict[tpl_id].nums
                loc_ext.result_supmode = tpl_id
                loc_ext_list.append(loc_ext)
                # 当 位置改变时 记录明细
                # if loc.SUPMODE != loc_ext.result_supmode:
                # 2024-1-22 16:39:15 蜀国小虾 【就把所有货格的变更前和变跟后的情况都在wops_flex_change_plan_dtl显示出来】
                plan_dtl = FlexChangePlanDtl(loc.LOC, loc.SUPMODE, loc_ext.result_supmode, plan_id, loc.HEATCODE)
                plan_dtl_list.append(plan_dtl)
                # 关系
                relation = LocTemplateRelation(tpl_id, loc.LOC)
                relation_list.append(relation)
            # 货位调整计划明细
            # FlexService.save_flex_plan_dtl(plan_dtl_list)
            all_plan_dtl_list.append(plan_dtl_list)
            # 关系存储
            # LocTemplateRelationService.sava_data(relation_list)
            all_relation_list.append(relation_list)
        # 货格 数据统计
        change_div_total_num: int = 0
        befor_change_div_total_num: int = 0
        after_change_div_total_num: int = 0
        for ext in loc_ext_list:
            befor_change_div_total_num += ext.raw_spec_nums
            after_change_div_total_num += ext.result_spec_nums
            if ext.supmode_change():
                change_div_total_num += 1
        # 调整计划
        fcp.change_div_total_num = change_div_total_num
        fcp.befor_change_div_total_num = befor_change_div_total_num
        fcp.after_change_div_total_num = after_change_div_total_num
        # FlexService.save_flex_plan(fcp)
        # 货位调整统计
        loc_spec_map = {}
        for ext in loc_ext_list:
            # 调整前货位 # 可能模板里面没有SUPMODE数据??
            if ext.loc.SUPMODE in self.loc_template_all_id_dict:
                before: LocTemplate = self.loc_template_all_id_dict[ext.loc.SUPMODE]
                key1 = f'{before.allow_loc_length}_{before.allow_loc_width}_{ext.loc.HEIGHT}_{ext.loc.HEATCODE}'
                if key1 not in loc_spec_map:
                    loc_spec_map[key1] = {'before': 0, 'after': 0}
                loc_spec_map[key1]['before'] += before.nums
            # 调整后货位
            after: LocTemplate = self.loc_template_id_dict[ext.result_supmode]
            key2 = f'{after.allow_loc_length}_{after.allow_loc_width}_{ext.loc.HEIGHT}_{ext.loc.HEATCODE}'
            if key2 not in loc_spec_map:
                loc_spec_map[key2] = {'before': 0, 'after': 0}
            loc_spec_map[key2]['after'] += after.nums
        flncr_list: list[FlexLocNumChangeRecord] = []
        items_list = loc_spec_map.items()
        # 计算更换前后 百分比
        before_change_div_num_list = [f"{_[0]}_{_[1]['before']}" for _ in items_list]
        before_change_div_percent_list = parse_percent(before_change_div_num_list)
        after_change_div_num_list = [f"{_[0]}_{_[1]['after']}" for _ in items_list]
        after_change_div_percent_list = parse_percent(after_change_div_num_list)
        for idx, kv in enumerate(items_list):
            idx: int
            k, v = kv
            length, width, height, heat_code = k.split('_')
            before: int = v['before']
            after: int = v['after']
            before_percent = before_change_div_percent_list[idx]
            after_percent = after_change_div_percent_list[idx]
            #
            flncr = FlexLocNumChangeRecord(length, width, height, before_percent, after_percent, before, after, plan_id,
                                           '', heat_code)
            spec_num = f'{height}*{length}*{width}'
            # 异常时，不设置
            if spec_num in self.tpl_ratio_dict and heat_code in self.tpl_ratio_dict[spec_num]:
                standard_percent = self.tpl_ratio_dict[spec_num][heat_code]
                difference_percent = standard_percent - after_percent
                flncr.stand_change_percent = standard_percent
                flncr.difference_percent = difference_percent
            #
            flncr_list.append(flncr)
        # FlexService.save_loc_num_change(flncr_list)
        PmOutService.save_result(fcp, all_plan_dtl_list, flncr_list, all_relation_list)
        msg = f'[PM] [{self.context.task_id}] 后处理 结束'
        self.context.app.logger.info(msg)

    def parse_row_list(self, zone_data: list[Loc]):
        """
        一个一维列表，列表中的元素为元组，
        从货架顶自左向右，自上而下遍历所有货格，
        每个元组内的元素为：（层数，货格序号，货格号，选取的货物类型，货量，热度，高度）
        :param zone_data:
        :return:
        """
        result = []
        max_layer = max([_.LAYER for _ in zone_data])
        sorted_zone_list: list[Loc] = sorted([_ for _ in zone_data],
                                             key=lambda _: int(f'{max_layer - _.LAYER}{_.LINE}{_.LIE:02d}'))
        for idx, d in enumerate(sorted_zone_list):
            d: Loc = d
            # 货格 货位占用数量
            nums = 0
            # 货格 会划分的模板 (无货物时，也将该参数输入)
            spec = 0
            if d.SUPMODE in self.loc_template_id_dict:
                spec = self.loc_template_id_dict[d.SUPMODE].inner_spec(d.HEIGHT)
            else:
                # self.context.app.logger.error(f'[PM] [{self.context.task_id}] LOC SUPMODE not found in loc_template. LOC: {d.LOC}, SUPMODE: {d.SUPMODE}')
                # 日志可能很多 无意义
                pass
            if d.LOC in self.las_dict:
                nums = self.las_dict[d.LOC]
            row = (d.LAYER, idx + 1, d.spec_no, spec, nums, d.heat_val, str(d.HEIGHT))
            result.append(row)
        return result, sorted_zone_list

    @staticmethod
    def parse_zone_list(full_zone_list: list[str], zone_list: list[str]) -> list[str]:
        """
        根据实际数据，解析传入参数
        :param full_zone_list: ['201-J', '201-O', '202-J', '202-O',]
        :param zone_list: ['201-J', '202']
        :return:  ['201-J', '202-J', '202-O',]
        """
        parsed_zone_list: list[str] = []
        for fz in full_zone_list:
            match = False
            for z in zone_list:
                if fz.startswith(z):
                    match = True
                    break
            if match:
                parsed_zone_list.append(fz)
        return parsed_zone_list


if __name__ == '__main__':
    pm_task = XPMTaskService()
    ctx = XpmContext()
    ctx.app = Flask(__name__)
    # 指定巷道
    ctx.zone_list = ['207']
    pm_task.run(ctx)
